package com.ilikesoup.instant.logger;

import net.sf.cglib.beans.BeanCopier;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.beans.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.function.Function;

/**
 * 该类主要用于debug
 * <p>
 * 监控需要关注对象的属性值变化，被监控对象的类要符合java bean规范
 * <p>
 * 该类提供了一系列静态方法来获取被监控器包装的对象
 * {@link BeanMonitor#wrap(Object)} 等方法用于包装一个已有对象，被包装对象的属性将会浅拷贝一份到返回对象中
 * {@link BeanMonitor#newInstance(Class)} 等方法用于生成一个被监控器包装的对象
 *
 * @author soup
 */
public class BeanMonitor implements MethodInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(BeanMonitor.class);
    /**
     * 当某个writer方法，没有对应的reader方法时，无法获得原属性值，将由该字符串代替
     */
    private static final String CAN_NOT_READ = "Can not read";
    /**
     * 默认的属性变更监听器
     */
    public static final List<PropertyChangeListener> DEFAULT_PROPERTY_CHANGE_LISTENERS = Collections.singletonList(new PropertyChangeLogListener());
    /**
     * 属性变化监听器组件
     */
    private PropertyChangeSupport propertyChangeSupport;
    /**
     * 需要监控的属性集合
     * writer方法名 -> 属性描述对象
     */
    private Map<String, PropertyDescriptor> propMap;
    /**
     * 对象转String的方法，使对象可读
     * 一般指定为toString方法；对于没有重写toString的类，将其转为json字符串
     */
    private Function<Object, String> toStrFunc;
    /**
     * 父监控器，如果存在，则将事件传播到父监控器处理
     */
    private BeanMonitor parent;
    /**
     * 属性的前缀，用于识别嵌套属性
     */
    private String propPrefix;
    /**
     * 深度代理，被代理类的属性的类型也会被代理
     * 判断一个类是否需要代理，见 {@link BeanMonitor#needProxy(Class)}
     */
    private boolean deepProxy;

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        Object retValFromSuper = null;
        PropertyDescriptor pd = null;
        String oldVal = null;
        try {
            if (propMap != null && (pd = propMap.get(method.getName())) != null) {
                oldVal = getOldVal(obj, pd);
                logger.debug("调用路径：{}", Thread.currentThread().getStackTrace()[3]);
                Object arg;
                if (deepProxy && (arg = args[0]) != null && needProxy(arg.getClass())) {
                    args[0] = wrap(arg, this);
                }
            }
            if (!Modifier.isAbstract(method.getModifiers())) {
                retValFromSuper = proxy.invokeSuper(obj, args);
            }
        } finally {
            if(pd != null) {
                PropertyChangeEvent changeEvent = new PropertyChangeEvent(toStrFunc.apply(obj), propPrefix + pd.getName(), oldVal, toStrFunc.apply(args[0]));
                changeEvent.setPropagationId(System.identityHashCode(this));
                firePropertyChange(changeEvent);
            }
        }
        return retValFromSuper;
    }

    private boolean needProxy(Class<?> clazz) {
        return !Modifier.isFinal(clazz.getModifiers()) && !clazz.getName().startsWith("java.");
    }

    private String getOldVal(Object obj, PropertyDescriptor pd) {
        String oldVal;
        Method readMethod = pd.getReadMethod();
        if (readMethod != null) {
            if (!readMethod.isAccessible()) {
                readMethod.setAccessible(true);
            }
            try {
                oldVal = toStrFunc.apply(readMethod.invoke(obj));
            } catch (IllegalAccessException | InvocationTargetException e) {
                oldVal = CAN_NOT_READ;
                logger.error("调用[{}]的reader方法[{}]失败", obj, pd.getReadMethod(), e);
            }
        } else {
            oldVal = CAN_NOT_READ;
        }
        return oldVal;
    }

    /**
     * 包装对象，监控其所有writer方法，一旦调用并使值改变时产生一条日志
     *
     * @param obj 被监控对象
     * @param <T> 对象类型
     * @return 被监控对象的可监控复制
     */
    public static <T> T wrap(T obj) {
        return wrap(obj, Objects::toString, DEFAULT_PROPERTY_CHANGE_LISTENERS, false);
    }

    /**
     * 包装对象，监控其所有writer方法，一旦调用并使值改变时产生一条日志
     *
     * @param obj 被监控对象
     * @param deepProxy 默认为false，如果设置为true则会为属性添加代理
     * @param <T> 对象类型
     * @return 被监控对象的可监控复制
     */
    public static <T> T wrap(T obj, boolean deepProxy) {
        return wrap(obj, Objects::toString, DEFAULT_PROPERTY_CHANGE_LISTENERS, deepProxy);
    }

    /**
     * 包装对象，监控其所有writer方法，一旦调用并使值改变时产生一条日志
     *
     * @param obj 被监控对象
     * @param <T> 对象类型
     * @return 被监控对象的可监控复制
     */
    private static <T> T wrap(T obj, BeanMonitor parent) {
        return wrap(obj, Objects::toString, DEFAULT_PROPERTY_CHANGE_LISTENERS, parent, false);
    }

    /**
     * 包装对象，监控其所有writer方法，一旦调用并使值改变时产生一条日志
     *
     * @param obj          被监控对象
     * @param includeProps 需要监控的属性名称数组，若为空，默认为有writer方法的全部属性
     * @param <T>          对象类型
     * @return 被监控对象的可监控复制
     */
    public static <T> T wrap(T obj, String... includeProps) {
        return wrap(obj, Objects::toString, DEFAULT_PROPERTY_CHANGE_LISTENERS, false, includeProps);
    }

    /**
     * 包装对象，监控其所有writer方法，一旦调用并使值改变时产生一条日志
     *
     * @param obj          被监控对象，如果为null，将不做处理
     * @param toStrFunc    对象转String的方法，若未指定，默认为 Objects::toString
     * @param propertyChangeListeners   属性改变监听器列表，当属性改变事件产生时，会回调监听器里的方法
     * @param deepProxy 默认为false，如果设置为true则会为属性添加代理
     * @param includeProps 需要监控的属性名称数组，若为空，默认为有writer方法的全部属性
     * @param <T>          对象类型
     * @return 被监控对象的可监控复制
     */
    public static <T> T wrap(T obj, Function<Object, String> toStrFunc, List<PropertyChangeListener> propertyChangeListeners, boolean deepProxy, String... includeProps) {
        return wrap(obj, toStrFunc, propertyChangeListeners, null, deepProxy, includeProps);
    }

    /**
     * 包装对象，监控其所有writer方法，一旦调用并使值改变时产生一条日志
     *
     * @param obj          被监控对象，如果为null，将不做处理
     * @param toStrFunc    对象转String的方法，若未指定，默认为 Objects::toString
     * @param propertyChangeListeners   属性改变监听器列表，当属性改变事件产生时，会回调监听器里的方法
     * @param parent       父监控器，事件将交给最顶级的父监控器处理
     * @param deepProxy    默认为false，如果设置为true则会为属性添加代理
     * @param includeProps 需要监控的属性名称数组，若为空，默认为有writer方法的全部属性
     * @param <T>          对象类型
     * @return 被监控对象的可监控复制
     */
    private static <T> T wrap(T obj, Function<Object, String> toStrFunc, List<PropertyChangeListener> propertyChangeListeners, BeanMonitor parent, boolean deepProxy, String... includeProps) {
        if (obj == null) {
            return null;
        }
        try {
            BeanMonitor interceptor = new BeanMonitor();
            Enhancer e = new Enhancer();
            e.setSuperclass(obj.getClass());
            e.setCallback(interceptor);
            Object bean = e.create();
            initMonitor(toStrFunc, interceptor, bean, propertyChangeListeners, obj.getClass(), includeProps);
            interceptor.parent = parent;
            interceptor.deepProxy = deepProxy;
            BeanCopier beanCopier = BeanCopier.create(obj.getClass(), bean.getClass(), false);
            beanCopier.copy(obj, bean, null);
            return (T) bean;
        } catch (Throwable e) {
            logger.error("BeanMonitor's creation failed!", e);
            return obj;
        }
    }

    /**
     * 为指定类，创建一个可监控的对象
     *
     * @param clazz 可监控对象类型
     * @param <T>   可监控对象类型
     * @return 相应类型的可监控属性的对象
     */
    public static <T> T newInstance(Class<T> clazz) {
        return newInstance(clazz, Objects::toString, DEFAULT_PROPERTY_CHANGE_LISTENERS, false);
    }

    /**
     * 为指定类，创建一个可监控的对象
     *
     * @param clazz 可监控对象类型
     * @param deepProxy    默认为false，如果设置为true则会为属性添加代理
     * @param <T>   可监控对象类型
     * @return 相应类型的可监控属性的对象
     */
    public static <T> T newInstance(Class<T> clazz, boolean deepProxy) {
        return newInstance(clazz, Objects::toString, DEFAULT_PROPERTY_CHANGE_LISTENERS, deepProxy);
    }

    /**
     * 为指定类，创建一个可监控的对象
     *
     * @param clazz        可监控对象类型
     * @param <T>          可监控对象类型
     * @param includeProps 需要监控的属性名称数组，若为空，默认为有writer方法的全部属性
     * @return 相应类型的可监控属性的对象
     */
    public static <T> T newInstance(Class<T> clazz, String... includeProps) {
        return newInstance(clazz, Objects::toString, DEFAULT_PROPERTY_CHANGE_LISTENERS, false, includeProps);
    }

    /**
     * 为指定类，创建一个可监控的对象
     *
     * @param clazz        可监控对象类型
     * @param toStrFunc    对象转String的方法，若未指定，默认为 Objects::toString
     * @param propertyChangeListeners   属性改变监听器列表，当属性改变事件产生时，会回调监听器里的方法
     * @param deepProxy    默认为false，如果设置为true则会为属性添加代理
     * @param includeProps 需要监控的属性名称数组，若为空，默认为有writer方法的全部属性
     * @param <T>          监控对象的类型
     * @return 相应类型的可监控属性的对象
     */
    public static <T> T newInstance(Class<T> clazz, Function<Object, String> toStrFunc, List<PropertyChangeListener> propertyChangeListeners, boolean deepProxy, String... includeProps) {
        try {
            BeanMonitor interceptor = new BeanMonitor();
            Enhancer e = new Enhancer();
            e.setSuperclass(clazz);
            e.setCallback(interceptor);
            Object bean = e.create();
            initMonitor(toStrFunc, interceptor, bean, propertyChangeListeners, clazz, includeProps);
            interceptor.deepProxy = deepProxy;
            return (T) bean;
        } catch (Throwable e) {
            logger.error("BeanMonitor's creation failed!", e);
            try {
                return clazz.newInstance();
            } catch (InstantiationException | IllegalAccessException ex) {
                logger.error("Bean's creation failed!", ex);
                return null;
            }
        }
    }

    private static void initMonitor(Function<Object, String> toStrFunc, BeanMonitor interceptor, Object bean,
                                    List<PropertyChangeListener> propertyChangeListeners, Class<?> clazz, String[] props) throws IntrospectionException {
        interceptor.propertyChangeSupport = new PropertyChangeSupport(bean);
        Optional.ofNullable(propertyChangeListeners).filter(o -> !o.isEmpty()).orElse(DEFAULT_PROPERTY_CHANGE_LISTENERS)
                .forEach(interceptor.propertyChangeSupport::addPropertyChangeListener);
        interceptor.propMap = initPropMap(clazz, props);
        interceptor.toStrFunc = Optional.ofNullable(toStrFunc).orElse(Objects::toString);
        interceptor.propPrefix = lowerFirst(clazz.getSimpleName()) + ".";
    }

    private static String lowerFirst(String str) {
        char[] chars = str.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }

    private static Map<String, PropertyDescriptor> initPropMap(Class<?> clazz, String... includeProps) throws IntrospectionException {
        Set<String> includePropSet = null;
        if (includeProps != null && includeProps.length > 0) {
            includePropSet = new HashSet<>(Arrays.asList(includeProps));
        }
        BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
        Map<String, PropertyDescriptor> propMap = new HashMap<>();
        for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
            if (includePropSet == null || includePropSet.contains(propertyDescriptor.getName())) {
                Optional.ofNullable(propertyDescriptor.getWriteMethod()).ifPresent(method -> propMap.put(method.getName(), propertyDescriptor));
            }
        }
        return propMap;
    }

    private void firePropertyChange(PropertyChangeEvent changeEvent) {
        firePropertyChange(propertyChangeSupport, changeEvent);
    }

    private void firePropertyChange(PropertyChangeSupport propertyChangeSupport, PropertyChangeEvent changeEvent) {
        if (changeEvent != null) {
            if (parent != null && !Objects.equals(changeEvent.getPropagationId(), System.identityHashCode(parent))) {
                parent.firePropertyChange(createPropagateEvent(changeEvent, parent.propPrefix));
                return;
            }
            if (propertyChangeSupport != null) {
                propertyChangeSupport.firePropertyChange(changeEvent);
            }
        }
    }

    private PropertyChangeEvent createPropagateEvent(PropertyChangeEvent changeEvent, String propPrefix) {
        PropertyChangeEvent newEvent = new PropertyChangeEvent(
                changeEvent.getSource(), propPrefix + changeEvent.getPropertyName(), changeEvent.getOldValue(), changeEvent.getNewValue());
        newEvent.setPropagationId(changeEvent.getPropagationId());
        return newEvent;
    }


    public static class PropertyChangeLogListener implements PropertyChangeListener {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            logger.debug("Bean属性变化监听，对象信息：{}，变更信息：[propName:{}, oldVal:{}, newVal:{}]",
                    evt.getSource(), evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
        }
    }

}
